package com.gitee.wsl.test


import com.gitee.wsl.func.predicate.Predicate
import kotlinx.coroutines.TimeoutCancellationException
import kotlin.math.abs

infix fun <T> T.shouldBe(any: T?): Unit = shouldEqual(any)

infix fun <T> T.shouldEqual(any: Any?) {
    if (this == any) return
    @Suppress("UNCHECKED_CAST")
    when (any) {
        is Predicate<*> -> (any as Predicate<T>).test(this)
        else -> {
            if (this == null && any != null)
                throw AssertionError("$this did not equal $any")
            if (this != null && any == null)
                throw AssertionError("$this did not equal $any")
            if (this != any)
                throw AssertionError("$this did not equal $any")
        }
    }
}

internal val epsilon = 1e-6
infix fun Double.shouldBe(other: Double) = ToleranceMatcher(other, 0.0).test(this)
infix fun Double.shouldBeClose(other: Double) = ToleranceMatcher(other, epsilon).test(this)
infix fun Float.shouldBeClose(other: Float) = ToleranceMatcher(other.toDouble(), epsilon).test(this.toDouble())


class ToleranceMatcher(val expected: Double, val tolerance: Double) : Predicate<Double> {

    override fun test(value: Double):Boolean {
        if (tolerance == 0.0 && (!expected.isNaN() || !value.isNaN()))
            println("[WARN] When comparing doubles ($expected, $value) consider using tolerance, eg: a shouldBe b plusOrMinus c ")
        if (value.isNaN() && !expected.isNaN()) throw AssertionError("$value is not equal to $expected")
        val diff = abs(value - expected)
        if (diff > tolerance)
            throw AssertionError("$value is not equal to $expected")
        return true
    }

    //infix fun plusOrMinus(tolerance: Double): ToleranceMatcher = ToleranceMatcher(expected, tolerance)
}


/**
 * Add [clue] as additional info to the assertion error message in case an assertion fails.
 * Can be nested, the error message will contain all available clues.
 *
 * @param thunk the code with assertions to be executed
 * @return the return value of the supplied [thunk]
 */
inline fun <R> withClue(clue: Any?, thunk: () -> R): R {
    return clue.asClue { thunk() }
}

/**
 * Similar to [withClue] but accepts a lazy in the case that a clue is expensive or is only valid when an assertion fails.
 * Can be nested, the error message will contain all available clues.
 *
 * @param thunk the code with assertions to be executed
 * @return the return value of the supplied [thunk]
 */
inline fun <R> withClue(crossinline clue: () -> Any? , thunk: () -> R): R {
    //val collector = errorCollector
    try {
        //collector.pushClue { clue.invoke().toString() }
        return thunk()
        // this is a special control exception used by coroutines
    } catch (t: TimeoutCancellationException) {
        //throw Exceptions.createAssertionError(clueContextAsString() + (t.message ?: ""), t)
        throw t
    } finally {
        //collector.popClue()
    }
}


/**
 * Similar to `withClue`, but will add `this` as a clue to the assertion error message in case an assertion fails.
 * Can be nested, the error message will contain all available clues.
 * `Lazy` and `Function0` are treated as lazy clues, so they will be evaluated only if an assertion fails.
 *
 * @param block the code with assertions to be executed
 * @return the return value of the supplied [block]
 */
@Suppress("USELESS_CAST")
inline fun <T : Any?, R> T.asClue(block: (T) -> R): R =
    withClue(
        // The cast is needed to avoid calling withClue(Any)
        when (this) {
            is Lazy<*> -> ({ value })
            //is Function0<*> -> ({ invoke() })
            else -> ({ this })
        } as () -> Any?,
    ) { block(this) }


inline fun <T : Any?> Iterable<T>.forEachAsClue(action: (T) -> Unit) = forEach { element ->
    element.asClue {
        action(it)
    }
}